<?php

namespace app\helpclass;

use yii;

/**
 * redis 分布式锁
 * Class RedisLock
 * @package app\helpclass
 */
class RedisLock
{
    /**
     * @var RedisStore
     */
    public $redis;

    public function __construct($redis_connect)
    {
        $redis = $redis_connect;
        $this->redis = yii::$app->$redis;
    }

    /**
     * redis上锁操作
     * @param $lockKey
     * @param $lockExpire
     * @param callable|null $call_func
     * @param $allinfo
     */
    public function doLock($lockKey, $lockExpire, callable $call_func = null, &$allinfo)
    {
        list($lockKey, $lockExpire, $call_func) = func_get_args();
        $status = true;
        while ($status) {
            //设置锁值为当前时间戳 + 有效期
            $lock_value = time() + (int)$lockExpire;
            /**
             * 创建锁
             * 以$lockKey 为key创建一个缓存,value 值为当前时间戳
             * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
             * 所以,用此函数就可以判定当前执行的操作是否已经有其他进程在执行了
             */
            $lock = $this->redis->setnx($lockKey, $lock_value);

            /**
             * 满足2个条件中的一个即可进行操作
             * 1.判定锁的值 是否小于当前时间 get()
             * 2.同时给锁设置新值 getset()
             */
            //代表上锁
            if (!empty($lock) || ($this->redis->get($lockKey) - time() < 0 && $this->redis->getset($lockKey, $lock_value) - time() < 0)) {
                //给锁设置生存时间
                $this->redis->expire($lockKey, $lockExpire);
                //进行回调方法执行
                $allinfo = call_user_func($call_func);

                $status = false;
            } else {
                sleep(2); //等待2秒再次执行
            }
        }
    }


    /**
     * 手动加锁，手动释放锁，失败会重试
     * @param string $lockKey 锁关键字
     * @param int $lockExpire 超时时间，单位秒
     * @param int $wait_micro_seconds 加锁失败等待时间，单位毫秒
     * @param int $retry 加锁失败重试次数，0不重试（总共执行一次加锁尝试并返回结果），1重试一次（最多执行2此加锁尝试并返回结果）
     * @return bool 加锁结果，false加锁未成功，true加锁成功
     */
    public function doLockWithFailRetry($lockKey, $lockExpire, $wait_micro_seconds = 200, $retry = 0)
    {
        // 总共执行次数=重试次数+1 ，保证至少执行一次
        $do_times = $retry + 1;

        // 是否所成功标记，false未成功，true成功
        $lock_status = false;
        while ($do_times > 0) {
            //设置锁值为当前时间戳 + 有效期
            $lock_value = time() + (int)$lockExpire;
            /**
             * 创建锁
             * 以$lockKey 为key创建一个缓存,value 值为当前时间戳
             * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
             * 所以,用此函数就可以判定当前执行的操作是否已经有其他进程在执行了
             */
            $lock = $this->redis->setnx($lockKey, $lock_value);

            /**
             * 满足2个条件中的一个即可进行操作
             * 1.判定锁的值 是否小于当前时间 get()
             * 2.同时给锁设置新值 getset()
             */
            if (!empty($lock) || ($this->redis->get($lockKey) - time() < 0 && $this->redis->getset($lockKey, $lock_value) - time() < 0)) {
                //代表上锁，给锁设置生存时间
                $this->redis->expire($lockKey, $lockExpire);
                $lock_status = true;
                $do_times = 0;
            } else {
                //上锁失败，延迟重试
                if ($wait_micro_seconds > 0) {
                    usleep($wait_micro_seconds * 1000);
                }
                $do_times--;
            }
        }

        return $lock_status;
    }

    /**
     * redis 解锁操作
     */
    public function unLock($lockKey)
    {
        if ($this->redis->ttl($lockKey)) {
            //代表 没过期 删除锁
            $this->redis->del($lockKey);
        }
        return true;
    }
}
